Trabajo de fin de máster

Máster Universitario en Ciencia de Datos

Estudios de Informática, Multimedia y Telecomunicación

Azucena González Muiño

 

Explicabilidad del modelo y fairness

Para el modelo de vigilancia policial definido anteriormente, se aplican técnicas de explicabilidad agnósticas del modelo (LIME y SHAP) para entender mejor su comportamiento. Para revisar si presenta sesgos no deseados, se emplea además la librería Aequitas, que permite calcular fácilmente diversas métricas de equidad y realizar una comparativa visual.

Este notebook se divide en los siguientes apartados:

  • Definición de funciones y variables globales
  • Carga del modelo y predicciones sobre el conjunto de test
  • Técnicas de explicabilidad
  • Revisión de la equidad del modelo
In [1]:
# Manipulación de datos
import numpy as np
from numpy.random import rand
from numpy.random import seed
import pandas as pd

# Construcción y evaluación de modelos
from sklearn.tree import DecisionTreeClassifier
from sklearn import tree
from sklearn import metrics

# Serialización de objetos
import pickle

# Tratamiento de fechas y timestamps
from datetime import datetime

# Librería para el estudio del fairness
from aequitas.group import Group
from aequitas.bias import Bias
from aequitas.fairness import Fairness
from aequitas.plotting import Plot
import aequitas.plot as ap

# LIME
import lime
import lime.lime_tabular
from lime import submodular_pick

# SHAP
import shap

# Generación de gráficas
import seaborn as sns
import matplotlib.pyplot as plt
import graphviz
%matplotlib inline

# Limitación del número de columnas a mostrar
pd.set_option('display.max_columns', None)

0. Definición de funciones

Este apartado recoge varias funciones y constantes que se emplean en las siguientes secciones.

In [2]:
# Etiquetas de clase
labels = ['False', 'True']

# Nombre de la variable objetivo
target = 'Further-action'

# Función que muestra gráficamente la matriz de confusión recibida como entrada indicando en
# cada celda el número de registros y el porcentaje que representan sobre el total
def conf_matrix_plot(conf_matrix, title='Matriz de confusión', labels=None, summary=False):
    # Se convierte la matriz a dataframe
    conf_matrix_df = pd.DataFrame(conf_matrix, columns=labels, index=labels)

    # Se generan los conjuntos de etiquetas a mostrar: nombres, total de registros y porcentaje
    # sobre el total
    group_names = ['TN','FP','FN','TP']
    group_counts = ['{0:.0f}'.format(value) for value in conf_matrix.flatten()]
    group_percentages = ['{0:.2%}'.format(value) for value in conf_matrix.flatten()/np.sum(conf_matrix)]
    ext_labels = [f'{l1}\n{l2}\n{l3}' for l1, l2, l3 in zip(group_names, group_counts, group_percentages)]
    ext_labels = np.asarray(ext_labels).reshape(2, 2)
    
    # Se crea un heatmap con la matriz de confusión
    sns.heatmap(conf_matrix_df, annot=ext_labels, fmt='', cmap='Blues', linewidths=2, cbar_kws={"shrink": .8})
    
    plt.title(title, fontsize=14)
    plt.ylabel('Valores reales', fontsize=12)
    plt.xlabel('Predicciones', fontsize=12)
    plt.yticks(rotation=0)
    
    return plt

# Función que devuelve una gráfica con las características de mayor importancia para el modelo indicado
def features_by_importance_plot(features, model, top=None):
    feat_importance = pd.DataFrame(sorted(zip(features, model.feature_importances_), reverse=True, key=lambda x: x[1]),
                                   columns=['Feature', 'Importance'])
    if top is None:
        top = len(features)

    plt.figure(figsize=(15, 5))
    sns.set_style('whitegrid')
    sns.barplot(x='Feature', y='Importance', data=feat_importance.head(top), 
                palette='Blues_r')
    plt.xlabel('Característica', size=12)
    plt.ylabel('Importancia', size=12)
    plt.title('Características con mayor importancia', size=14)
    plt.xticks(rotation=90)
    
    return plt

1. Carga del modelo y realización de predicciones

Se recuperan desde fichero el modelo entrenado previamente y los conjuntos de datos usados.

In [3]:
# Se carga el modelo ya entrenado y los conjuntos de datos
model = pickle.load(open('model.sav', 'rb'))
orig_data = pickle.load(open('sel_data.sav', 'rb'))
X_train = pickle.load(open('x_train.sav', 'rb'))
y_train= pickle.load(open('y_train.sav', 'rb'))
X_val = pickle.load(open('x_val.sav', 'rb'))
y_val = pickle.load(open('y_val.sav', 'rb'))
X_test = pickle.load(open('x_test.sav', 'rb'))
y_test = pickle.load(open('y_test.sav', 'rb'))

# Variables empleadas por el modelo
features = X_test.columns

print('Mejor modelo obtenido:', model)
print('Características seleccionadas:', features.tolist())
Mejor modelo obtenido: RandomForestClassifier(class_weight='balanced', criterion='entropy',
                       max_depth=5, max_features='sqrt', min_samples_leaf=5,
                       n_estimators=50, random_state=22)
Características seleccionadas: ['Year', 'Income', 'Type_Person and Vehicle search', 'Type_Person search', 'Type_Vehicle search', 'Officer-ethnicity_Asian', 'Officer-ethnicity_Black', 'Officer-ethnicity_Other', 'Officer-ethnicity_Unknown', 'Officer-ethnicity_White']

Se ejecuta el modelo sobre los datos de prueba: sobre sus resultados se aplicarán posteriormente algunas técnicas de explicabilidad y equidad de modelos.

In [4]:
# Predicción para el conjunto de prueba
y_pred = model.predict(X_test)

# Generación de la matriz de confusión y del informe de métricas
tree_conf_matrix = metrics.confusion_matrix(y_test, y_pred)
tree_class_metrics = metrics.classification_report(y_test, y_pred, output_dict=True, zero_division=0)   

print(metrics.classification_report(y_test, y_pred, output_dict=False, zero_division=0))
conf_matrix_plot(tree_conf_matrix, 'Matriz de confusión: conjunto de test', labels).show()
              precision    recall  f1-score   support

       False       0.79      0.77      0.78    180168
        True       0.32      0.35      0.33     55549

    accuracy                           0.67    235717
   macro avg       0.56      0.56      0.56    235717
weighted avg       0.68      0.67      0.68    235717

2. Técnicas de explicabilidad

La importancia que cada variable tiene en el modelo final es uno de los métodos más directos para entender a alto nivel el comportamiento del modelo.
En este caso, debido al one hot encoding aplicado a las variables categóricas, la importancia de las variables originales se diluye entre sus dummies, lo que puede dificultar la comparativa.

In [5]:
# Se muestran las variables con mayor importancia
features_by_importance_plot(features, model).show()

La variable con mayor importancia en el modelo es claramente el tipo de búsqueda y, en concreto, sus dummies para registro sobre persona y registro sobre persona y vehículo. Es interesante ver que el año tiene también bastante relevancia. En la variable etnia predominan la blanca y la negra.

En el caso de modelos basados en árboles de decisión, pintar la estructura del árbol permite visualizar fácilmente sus nodos y las condiciones que se aplican en ellos, a qué profundidad aparecen las distintas variables y cómo se distribuyen las predicciones. Dado que un random forest se compone de varios árboles de decisión, pueden visualizarse también, aunque extraer una visión global del funcionamiento del modelo es complicado debido a la variación de cada árbol, máxime si el número de estos es elevado. En este caso, el mejor modelo encontrado hace uso de 50 estimadores diferentes, por lo que se emplearán técnicas como LIME y SHAP para interpretar el modelo.

No obstante, a modo ilustrativo, se muestra a continuación uno de los árboles predictores.

In [6]:
# Se visualiza uno de los estimadores
dot_data = tree.export_graphviz(model.estimators_[32],
                                out_file=None, 
                                feature_names=features,
                                class_names=labels,
                                filled=True,
                                rounded=True)
graph = graphviz.Source(dot_data, format='png')
graph.render('tree' + datetime.now().strftime('.%Y%m%d.%H%M'), view=True)
graph
Out[6]:
Tree 0 Officer-ethnicity_White <= 0.5 entropy = 1.0 samples = 243484 value = [192623.992, 192172.328] class = False 1 Income <= 35035.0 entropy = 0.999 samples = 154918 value = [124283.982, 117550.854] class = False 0->1 True 32 Income <= 43785.0 entropy = 0.999 samples = 88566 value = [68340.009, 74621.473] class = True 0->32 False 2 Type_Person and Vehicle search <= 0.5 entropy = 0.996 samples = 41609 value = [34217.071, 29716.386] class = False 1->2 17 Type_Person search <= 0.5 entropy = 1.0 samples = 113309 value = [90066.911, 87834.468] class = False 1->17 3 Type_Person search <= 0.5 entropy = 0.984 samples = 29142 value = [24904.756, 18468.08] class = False 2->3 10 Officer-ethnicity_Black <= 0.5 entropy = 0.994 samples = 12467 value = [9312.315, 11248.306] class = True 2->10 4 Income <= 29100.0 entropy = 0.997 samples = 952 value = [711.839, 815.254] class = True 3->4 7 Year <= 2017.5 entropy = 0.982 samples = 28190 value = [24192.917, 17652.826] class = False 3->7 5 entropy = 1.0 samples = 202 value = [158.034, 159.746] class = True 4->5 6 entropy = 0.995 samples = 750 value = [553.805, 655.509] class = True 4->6 8 entropy = 1.0 samples = 5290 value = [4172.78, 4283.757] class = True 7->8 9 entropy = 0.971 samples = 22900 value = [20020.137, 13369.069] class = False 7->9 11 Officer-ethnicity_Unknown <= 0.5 entropy = 1.0 samples = 7643 value = [6034.144, 6086.865] class = True 10->11 14 Year <= 2017.5 entropy = 0.964 samples = 4824 value = [3278.171, 5161.441] class = True 10->14 12 entropy = 1.0 samples = 7594 value = [6002.537, 6046.469] class = True 11->12 13 entropy = 0.989 samples = 49 value = [31.607, 40.395] class = True 11->13 15 entropy = 0.9 samples = 1145 value = [691.913, 1500.141] class = True 14->15 16 entropy = 0.979 samples = 3679 value = [2586.258, 3661.3] class = True 14->16 18 Year <= 2017.5 entropy = 0.974 samples = 30116 value = [21104.386, 30860.313] class = True 17->18 25 Income <= 38685.0 entropy = 0.993 samples = 83193 value = [68962.525, 56974.156] class = False 17->25 19 Officer-ethnicity_Black <= 0.5 entropy = 0.947 samples = 7199 value = [4765.063, 8262.712] class = True 18->19 22 Income <= 37070.0 entropy = 0.981 samples = 22917 value = [16339.323, 22597.6] class = True 18->22 20 entropy = 0.958 samples = 2583 value = [1740.433, 2844.209] class = True 19->20 21 entropy = 0.941 samples = 4616 value = [3024.63, 5418.503] class = True 19->21 23 entropy = 0.973 samples = 11067 value = [7690.75, 11400.707] class = True 22->23 24 entropy = 0.988 samples = 11850 value = [8648.573, 11196.893] class = True 22->24 26 Officer-ethnicity_Asian <= 0.5 entropy = 0.992 samples = 49058 value = [40943.128, 32959.041] class = False 25->26 29 Officer-ethnicity_Black <= 0.5 entropy = 0.996 samples = 34135 value = [28019.397, 24015.114] class = False 25->29 27 entropy = 0.99 samples = 40552 value = [33988.953, 26850.143] class = False 26->27 28 entropy = 0.997 samples = 8506 value = [6954.175, 6108.899] class = False 26->28 30 entropy = 0.999 samples = 10227 value = [8205.391, 7715.537] class = False 29->30 31 entropy = 0.993 samples = 23908 value = [19814.006, 16299.577] class = False 29->31 33 Type_Person search <= 0.5 entropy = 0.999 samples = 71190 value = [55239.005, 59008.619] class = True 32->33 48 Type_Person search <= 0.5 entropy = 0.994 samples = 17376 value = [13101.004, 15612.854] class = True 32->48 34 Year <= 2018.5 entropy = 0.954 samples = 13809 value = [9191.385, 15374.153] class = True 33->34 41 Year <= 2018.5 entropy = 0.999 samples = 57381 value = [46047.621, 43634.466] class = False 33->41 35 Income <= 35340.0 entropy = 0.939 samples = 7724 value = [4976.004, 9011.865] class = True 34->35 38 Income <= 42510.0 entropy = 0.97 samples = 6085 value = [4215.38, 6362.289] class = True 34->38 36 entropy = 0.925 samples = 2506 value = [1578.277, 3059.04] class = True 35->36 37 entropy = 0.945 samples = 5218 value = [3397.727, 5952.825] class = True 35->37 39 entropy = 0.967 samples = 5624 value = [3868.393, 5952.825] class = True 38->39 40 entropy = 0.995 samples = 461 value = [346.987, 409.463] class = True 38->40 42 Income <= 39335.0 entropy = 0.999 samples = 29601 value = [22840.697, 24946.047] class = True 41->42 45 Income <= 30780.0 entropy = 0.992 samples = 27780 value = [23206.923, 18688.419] class = False 41->45 43 entropy = 0.999 samples = 22941 value = [17754.756, 19125.425] class = True 42->43 44 entropy = 0.997 samples = 6660 value = [5085.941, 5820.622] class = True 42->44 46 entropy = 1.0 samples = 2716 value = [2210.412, 2104.237] class = False 45->46 47 entropy = 0.99 samples = 25064 value = [20996.511, 16584.182] class = False 45->47 49 Type_Vehicle search <= 0.5 entropy = 0.925 samples = 1823 value = [1138.531, 2210.735] class = True 48->49 56 Year <= 2018.5 entropy = 0.998 samples = 15553 value = [11962.473, 13402.119] class = True 48->56 50 Income <= 54545.0 entropy = 0.923 samples = 1787 value = [1114.482, 2179.52] class = True 49->50 53 Year <= 2018.5 entropy = 0.988 samples = 36 value = [24.049, 31.215] class = True 49->53 51 entropy = 0.949 samples = 1263 value = [830.708, 1430.367] class = True 50->51 52 entropy = 0.848 samples = 524 value = [283.774, 749.153] class = True 50->52 54 entropy = 0.965 samples = 25 value = [16.49, 25.706] class = True 53->54 55 entropy = 0.982 samples = 11 value = [7.558, 5.508] class = False 53->55 57 Year <= 2017.5 entropy = 0.991 samples = 8719 value = [6483.509, 8090.113] class = True 56->57 60 Income <= 59620.0 entropy = 1.0 samples = 6834 value = [5478.964, 5312.006] class = False 56->60 58 entropy = 0.986 samples = 4238 value = [3087.156, 4079.944] class = True 57->58 59 entropy = 0.995 samples = 4481 value = [3396.353, 4010.17] class = True 57->59 61 entropy = 1.0 samples = 6593 value = [5312.685, 5060.452] class = False 60->61 62 entropy = 0.97 samples = 241 value = [166.279, 251.554] class = True 60->62

El dibujo del árbol permite, de un primer vistazo, localizar uno de los problemas que ya se habían identificado previamente en el modelo: el poco margen existente en la clasificación de las observaciones. Los nodos finales del árbol presentan, en la mayoría de los casos, un nivel de impureza elevado, con muchos elementos tanto de la clase positiva como de la negativa.
En cuanto a su interpretación, el nodo raíz realiza una primera división según la etnia. El lado derecho se corresponde con los registros que pertenecen a personas blancas y el izquierdo con los que no.
Revisando el lado correspondiente a personas de etnia blanca, se localizan pocos nodos donde se clasifique el registro como negativo. La otra parte del árbol presenta más variedad en sus clasificaciones. Puede destacarse, por ejemplo, que en aquellos distritos con ingresos superiores a 35035 libras y cuyo registro sea sobre la persona, aunque aparecen otros nodos de decisión, terminan clasificándose las observaciones en la clase negativa. Si el registro no es sobre la persona, se llega a una rama del árbol en la que todos los registros se terminan clasificando como positivos.

2.1 LIME

LIME es una técnica de explicabilidad agnóstica del modelo que se basa en la aplicación de pequeños cambios en la entrada del modelo para observar cómo fluctúan las probabilidades a la salida.

In [7]:
# Preparación del objeto necesario para ejecutar LIME
explainer = lime.lime_tabular.LimeTabularExplainer(np.array(X_test),
                    feature_names=X_test.columns,
                    class_names=labels, 
                    verbose=False, mode='classification')

A continuación se seleccionan varios registros y se revisan individualmente empleando LIME. En azul se muestran las variables y los valores que ayudan a clasificar las actuaciones como negativas, y en naranja, los que aportan a la clase positiva.

Las dos primeras observaciones seleccionadas se corresponden a registros en distritos de renta alta, uno sobre una persona de etnia blanca y otro sobre una persona que no es blanca. Ambos son correctamente clasificados por el modelo: el primero es un registro positivo y el segundo negativo. En el segundo caso, obviando la etnia, es la variable de tipo de búsqueda la diferenciadora en la clasificación del registro como negativo.
Parece que estar en un distrito con rentas altas aumenta la probabilidad de que el modelo clasifique el registro como positivo.

In [8]:
# Distrito de renta alta, etnia blanca
idx_test1 = X_test.index[(X_test['Income']>45000) & (X_test['Officer-ethnicity_White']==1)][0]
example = X_test.loc[idx_test1]
print('Clase real:', y_test[idx_test1])
exp = explainer.explain_instance(example, model.predict_proba, num_features=6)
exp.show_in_notebook(show_all=True)
Clase real: True
In [9]:
# Distrito de renta alta, etnia no blanca
idx_test2 = X_test.index[(X_test['Income']>45000) & (X_test['Officer-ethnicity_White']==0)][0]
example = X_test.loc[idx_test2]
print('Clase real:', y_test[idx_test2])
exp = explainer.explain_instance(example, model.predict_proba, num_features=6)
exp.show_in_notebook(show_all=True)
Clase real: False

A continuación se repite el ejercicio con distritos de renta baja.
En este caso, el primero de los registros es correctamente clasificado como positivo, mientras que el segundo se clasifica como negativo siendo positivo.
Se observa el efecto complementario a los ejemplos vistos previamente: las rentas bajas aumentan la probabilidad de que se clasifique el registro como negativo. En cuanto a la raza, ser blanco o no ser negro aumentan la probabilidad de pertenecer a la clase positiva.

In [10]:
# Distrito de renta baja, etnia blanca
idx_test3 = X_test.index[(X_test['Income']<35000) & (X_test['Officer-ethnicity_White']==1)][0]
example = X_test.loc[idx_test3]
print('Clase real:', y_test[idx_test3])
exp = explainer.explain_instance(example, model.predict_proba, num_features=6)
exp.show_in_notebook(show_all=True)
Clase real: True
In [11]:
# Distrito de renta baja, etnia no blanca
idx_test4 = X_test.index[(X_test['Income']<35000) & (X_test['Officer-ethnicity_White']==0)][0]
example = X_test.loc[idx_test4]
print('Clase real:', y_test[idx_test4])
exp = explainer.explain_instance(example, model.predict_proba, num_features=6)
exp.show_in_notebook(show_all=True)
Clase real: True

LIME también permite realizar estudios sobre muestras del conjunto de datos. En el ejemplo siguiente se definen dos grupos, uno de personas de raza negra y otro para personas blancas.

In [12]:
# Se generan los dos grupos a comparar
subsample_black = X_test[(X_test['Officer-ethnicity_Black']==1)]
subsample_white = X_test[(X_test['Officer-ethnicity_White']==1)]

Se muestran a continuación las variables que más influyen en la toma de decisiones del modelo para una muestra de cada uno de los grupos definidos.

In [13]:
# Muestra para etnia negra
sp_obj = submodular_pick.SubmodularPick(explainer, 
                                        np.array(subsample_black),
                                        model.predict_proba,
                                        sample_size=100,
                                        num_features=6,
                                        num_exps_desired=1)
# Se muestran las explicaciones solicitadas
[exp.show_in_notebook() for exp in sp_obj.sp_explanations]
Out[13]:
[None]
In [14]:
# Muestra para etnia blanca
sp_obj = submodular_pick.SubmodularPick(explainer, 
                                        np.array(subsample_white),
                                        model.predict_proba,
                                        sample_size=100,
                                        num_features=6,
                                        num_exps_desired=1)
# Se muestran las explicaciones solicitadas
[exp.show_in_notebook() for exp in sp_obj.sp_explanations]
Out[14]:
[None]

Aunque se trata de una muestra sobre la población total, las gráficas anteriores corroboran lo observado en el análisis de importancia de características: la variable que más afecta a las predicciones es el tipo de registro. En cuanto al año, que el registro haya tenido lugar antes de 2021 aumenta la probabilidad de que el registro sea clasificado como negativo.

Con el uso de SHAP se puede visualizar el comportamiento de las variables para todo el conjunto de observaciones, lo que permitirá llegar a conclusiones sobre el comportamiento general del modelo.

2.2 SHAP

A continuación se utiliza SHAP para revisar la importancia de las características y su impacto (negativo y positivo) en la predicción de la variable objetivo.
En primer lugar, deben generarse los valores SHAP de cada observación del conjunto de datos.

In [15]:
# Se generan los objetos SHAP necesarios para el uso de esta librería y se activa javascript
shap.initjs()
shap_explainer = shap.TreeExplainer(model)
shap_values = shap_explainer.shap_values(X_test)

Las características más importantes calculadas por los valores SHAP son muy parecidas a las obtenidas previamente revisando la importancia asignada por el propio modelo. Sigue predominando el tipo de búsqueda y, aunque se producen algunos ligeros cambios de orden en algunas de las siguientes características (Officer-ethnicity_White e Income), siguen apareciendo las mismas en los primeros puestos.

In [16]:
# Impacto de las principales características en el modelo
shap.summary_plot(shap_values, X_test, max_display=10, class_inds=[1], class_names=['False', 'True'])

En la siguiente gráfica aparecen recogidas todas las observaciones del conjunto de datos:

  • Las características se muestran de mayor a menor relevancia o contribución a la predicción. Es decir, las variables que aparecen arriba en el eje Y tienen mayor impacto.
  • La ubicación horizontal indica si el efecto de esa observación está asociada con una predicción mayor o menor
  • El color rojo representa que la variable tiene un valor alto (1 en el caso de las dummies). El azul representa un valor bajo (0 para las dummies).
  • La correlación con la variable objetivo se muestra en el eje X: por debajo de 0 es negativa y por encima es positiva.
In [17]:
# Se muestra el impacto de las características para cada observación del conjunto de datos seleccionado
# en relación a la clase positiva
shap.summary_plot(shap_values[1], X_test)

De la gráfica anterior puede deducirse que un cacheo sobre una persona (variable a 1, color rojo) contribuye a que el modelo decida que la acción policial sea negativa (correlación negativa), mientras que si el registro no es sobre una persona aumenta la probabilidad de que la actuación policial sea positiva (es decir, que se encuentren indicios de delito). Este efecto se refuerza observando la información de la siguiente dummy relativa al tipo de registro, donde se muestra que actuaciones policiales sobre el individuo y su vehículo influyen positivamente en que el modelo clasifique la acción policial como positiva, y para actuaciones que no lo son lo haga en sentido contrario.

En cuanto a las etnias:

  • Ser de etnia blanca aumenta la probabilidad de que el registro sea positivo, mientras que no serlo la disminuye
  • Ser de etnia negra o asiática tiene una correlación negativa con una acción policial exitosa, es decir, ser de cualquiera de estas razas contribuye a una clasificación de registro infructuoso

Respecto al nivel de ingresos familiares, en general parece que los distritos más ricos influyen positivamente en clasificar las actuaciones como exitosas, mientras que los de rentas más bajas lo hacen en sentido contrario.

En cuanto al año, a pesar de ser una de las variables con mayor importancia en el modelo, todas las observaciones parecen mantener una correlación negativa con la clase positiva. Aparecen pocas observaciones en rojo porque en el conjunto de test están recogidos los registros más actuales y sólo se incluyen dos meses de 2021. Si se visualiza esta misma gráfica para el conjunto de entrenamiento, se entiende el comportamiento del modelo: los años menos recientes tienen un impacto positivo en la predicción de registros exitosos y los más recientes tienen justo el efecto contrario.

In [18]:
# Visualización del impacto con el conjunto de entrenamiento
shap_explainer_tr = shap.TreeExplainer(model)
shap_values_tr = shap_explainer.shap_values(X_train)
shap.summary_plot(shap_values_tr[1], X_train)

SHAP también permite estudiar las observaciones una a una. En las siguientes gráficas, se recogen las predicciones en referencia a la clase positiva para los mismos registros revisados en LIME:

  • Los bloques en azul rebajan la probabilidad de la predicción realizada
  • Los bloques en rojo aumentan la probabilidad de la predicción realizada
  • Cuanto mayor es el bloque, más influencia tiene la variable

En el primer ejemplo, las variables que aumentan la probabilidad de que la clase sea positiva coinciden con las vistas en el estudio con LIME, a diferencia del año, que según SHAP reduce la probabilidad de predecir el registro como positivo.

In [19]:
# Renta alta, etnia blanca
shap_values_1 = shap_explainer.shap_values(X_test.loc[idx_test1])
shap.force_plot(shap_explainer.expected_value[1], shap_values_1[1], X_test.loc[idx_test1])
Out[19]:
Visualization omitted, Javascript library not loaded!
Have you run `initjs()` in this notebook? If this notebook was from another user you must also trust this notebook (File -> Trust notebook). If you are viewing this notebook on github the Javascript has been stripped for security. If you are using JupyterLab this error is because a JupyterLab extension has not yet been written.

En este caso, el nivel de ingresos (elevado, pertenece a un distrito con un alto nivel de vida) y que la etnia no sea negra, son las variables que aumentan la probabilidad de que el modelo clasifique la observación como positiva. Las que más aportan a rebajar esa probabilidad son el año, que el tipo de búsqueda sea sobre persona (y no sobre persona y vehículo) y que la raza no sea la blanca.

In [20]:
# Renta alta, etnia no blanca
shap_values_2 = shap_explainer.shap_values(X_test.loc[idx_test2])
shap.force_plot(shap_explainer.expected_value[1], shap_values_2[1], X_test.loc[idx_test2])
Out[20]:
Visualization omitted, Javascript library not loaded!
Have you run `initjs()` in this notebook? If this notebook was from another user you must also trust this notebook (File -> Trust notebook). If you are viewing this notebook on github the Javascript has been stripped for security. If you are using JupyterLab this error is because a JupyterLab extension has not yet been written.

El siguiente ejemplo, a excepción del año, también coincide con el análisis de LIME: el tipo de búsqueda y la etnia aumentan la probabilidad de una clasificación en la clase positiva, y el income la reduce.

In [21]:
# Renta baja, etnia blanca
shap_values_3 = shap_explainer.shap_values(X_test.loc[idx_test3])
shap.force_plot(shap_explainer.expected_value[1], shap_values_3[1], X_test.loc[idx_test3])
Out[21]:
Visualization omitted, Javascript library not loaded!
Have you run `initjs()` in this notebook? If this notebook was from another user you must also trust this notebook (File -> Trust notebook). If you are viewing this notebook on github the Javascript has been stripped for security. If you are using JupyterLab this error is because a JupyterLab extension has not yet been written.

En el último caso, la única variable que aumenta la probabilidad de una clasificación positiva es que la etnia no sea negra y lo hace además con muy poco impacto.

In [22]:
# Renta baja, etnia no blanca
shap_values_4 = shap_explainer.shap_values(X_test.loc[idx_test4])
shap.force_plot(shap_explainer.expected_value[1], shap_values_4[1], X_test.loc[idx_test4])
Out[22]:
Visualization omitted, Javascript library not loaded!
Have you run `initjs()` in this notebook? If this notebook was from another user you must also trust this notebook (File -> Trust notebook). If you are viewing this notebook on github the Javascript has been stripped for security. If you are using JupyterLab this error is because a JupyterLab extension has not yet been written.

3. Equidad del modelo

Para la revisión del modelo generado desde el punto de vista del fairness o la equidad algorítmica, se emplea la librería de Aequitas, implementada por la Universidad de Chicago.
Las métricas de equidad que se van a revisar son la tasa de falsos positivos y la de falsos negativos sobre los grupos de etnia y distrito (se utilizan los nombres de los distritos en lugar de sus ingresos para facilitar su interpretación).

In [23]:
# Para esta comparativa se pueden emplear las variables originales, sin one hot encoding.
# Se procede a seleccionar los registros de test e incorporar los valores predichos por el modelo
test_data = orig_data.tail(len(y_test)).copy(deep=True)

# La librería Aequitas necesita que los valores reales estén en una columna llamada 'label_value'
# y las predicciones en 'score'
test_data.rename(columns={'Further-action':'label_value'}, inplace=True)
test_data['score'] = y_pred
test_data.head()

# Se inicializan algunos elementos necesarios para la ejecución de la librería
g = Group()
b = Bias()
aqp = Plot()

# Se indican las variables a estudio y las métricas seleccionadas
attributes_to_audit = ['Officer-ethnicity', 'Borough']
metrics = ['fpr', 'fnr']

# Se obtiene un dataframe con las estadísticas por variable y grupo y las métricas de fairness calculadas
xtab, _ = g.get_crosstabs(test_data, attr_cols=attributes_to_audit)

# Para las gráficas de disparidad se toman como variables de referencia la etnia negra y el distrito de Lambeth (ya
# que tiene un nivel de ingresos familiares intermedio)
bdf = b.get_disparity_predefined_groups(xtab, test_data, 
                                        ref_groups_dict={'Officer-ethnicity':'Black', 'Borough':'Lambeth'},
                                        fill_divbyzero=0)

# Estadísticas y métricas para la etnia
xtab_eth = xtab[(xtab['attribute_name']=='Officer-ethnicity')]
xtab_eth
get_disparity_predefined_group()
Out[23]:
model_id score_threshold k attribute_name attribute_value tpr tnr for fdr fpr fnr npv precision pp pn ppr pprev fp fn tn tp group_label_pos group_label_neg group_size total_entities prev
0 0 binary 0/1 59986 Officer-ethnicity Asian 0.373237 0.743206 0.203727 0.693985 0.256794 0.626763 0.796273 0.306015 12104 30531 0.201780 0.283898 8400 6220 24311 3704 9924 32711 42635 235717 0.232767
1 0 binary 0/1 59986 Officer-ethnicity Black 0.350147 0.765906 0.202639 0.690604 0.234094 0.649853 0.797361 0.309396 21988 62308 0.366552 0.260843 15185 12626 49682 6803 19429 64867 84296 235717 0.230485
2 0 binary 0/1 59986 Officer-ethnicity Other 0.352804 0.746512 0.197787 0.716432 0.253488 0.647196 0.802213 0.283568 3195 8403 0.053262 0.275479 2289 1662 6741 906 2568 9030 11598 235717 0.221417
3 0 binary 0/1 59986 Officer-ethnicity Unknown 0.814669 0.356683 0.169664 0.667557 0.643317 0.185331 0.830336 0.332443 3742 1668 0.062381 0.691682 2498 283 1385 1244 1527 3883 5410 235717 0.282255
4 0 binary 0/1 59986 Officer-ethnicity White 0.295824 0.821763 0.213716 0.655114 0.178237 0.704176 0.786284 0.344886 18957 72821 0.316024 0.206553 12419 15563 57258 6538 22101 69677 91778 235717 0.240809

A continuación se muestran las gráficas de las tasas de falsos positivos y falsos negativos.

In [24]:
# Gráficas para FPR y FNR
ap.disparity(bdf, metrics, 'Officer-ethnicity')
Out[24]:

Puede observarse cómo las personas de raza blanca tienen una menor tasa de falsos positivos y también, aunque se sitúa dentro del umbral aceptable definido, una mayor tasa de falsos negativos. Para el caso de la etnia sin valor conocido, sorprende su disparidad con el resto de grupos. No es posible determinar si el origen de estos valores perdidos conlleva cierta intencionalidad por parte del oficial que realiza la intervención o si puede estar reflejando un problema de calidad de la información recogida, por lo que no se pueden extraer conclusiones que expliquen una diferencia tan notable.

Se representan a continuación los valores de las dos métricas indicadas, donde aparecen en rojo aquellos elementos que no cumplirían los criterios de fairness respcto al grupo de referencia establecido (etnia negra).

In [25]:
# Gráfica con la tasa de falsos positivos
f = Fairness()
fdf = f.get_group_value_fairness(bdf[bdf['attribute_name']=='Officer-ethnicity'].copy(deep=True))
fpr_fairness = aqp.plot_fairness_group(fdf, group_metric='fpr', title=True)
In [26]:
# Gráfica con la tasa de falsos negativos
f = Fairness()
fdf = f.get_group_value_fairness(bdf[bdf['attribute_name']=='Officer-ethnicity'].copy(deep=True))
fpr_fairness = aqp.plot_fairness_group(fdf, group_metric='fnr', title=True)

A continuación se realiza un estudio similar para los distritos. En este caso, como ya se indicaba previamente, van a utilizarse los nombres de los distritos en lugar de los ingresos familiares para facilitar el estudio.

In [27]:
# Estadísticas y métricas por distrito
xtab_bor = xtab[(xtab['attribute_name']=='Borough')]
xtab_bor
Out[27]:
model_id score_threshold k attribute_name attribute_value tpr tnr for fdr fpr fnr npv precision pp pn ppr pprev fp fn tn tp group_label_pos group_label_neg group_size total_entities prev
5 0 binary 0/1 59986 Borough Barking and Dagenham 0.389153 0.755777 0.224800 0.636248 0.244223 0.610847 0.775200 0.363752 1578 4008 0.026306 0.282492 1004 901 3107 574 1475 4111 5586 235717 0.264053
6 0 binary 0/1 59986 Borough Barnet 0.439922 0.690236 0.219939 0.669578 0.309764 0.560078 0.780061 0.330422 1374 2628 0.022905 0.343328 920 578 2050 454 1032 2970 4002 235717 0.257871
7 0 binary 0/1 59986 Borough Bexley 0.404588 0.738679 0.243082 0.618486 0.261321 0.595412 0.756918 0.381514 1017 2349 0.016954 0.302139 629 571 1778 388 959 2407 3366 235717 0.284908
8 0 binary 0/1 59986 Borough Brent 0.406087 0.714185 0.198285 0.702957 0.285815 0.593913 0.801715 0.297043 2875 6299 0.047928 0.313386 2021 1249 5050 854 2103 7071 9174 235717 0.229235
9 0 binary 0/1 59986 Borough Bromley 0.376409 0.766667 0.183102 0.692260 0.233333 0.623591 0.816898 0.307740 1628 4533 0.027140 0.264243 1127 830 3703 501 1331 4830 6161 235717 0.216036
10 0 binary 0/1 59986 Borough Camden 0.241007 0.838302 0.191346 0.719665 0.161698 0.758993 0.808654 0.280335 1673 7719 0.027890 0.178130 1204 1477 6242 469 1946 7446 9392 235717 0.207198
11 0 binary 0/1 59986 Borough City of London 0.279503 0.834990 0.216418 0.648438 0.165010 0.720497 0.783582 0.351562 384 1608 0.006401 0.192771 249 348 1260 135 483 1509 1992 235717 0.242470
12 0 binary 0/1 59986 Borough Croydon 0.376857 0.768635 0.203239 0.661160 0.231365 0.623143 0.796761 0.338840 2845 7843 0.047428 0.266186 1881 1594 6249 964 2558 8130 10688 235717 0.239334
13 0 binary 0/1 59986 Borough Ealing 0.399417 0.767658 0.197192 0.649467 0.232342 0.600583 0.802808 0.350533 2345 6268 0.039092 0.272263 1523 1236 5032 822 2058 6555 8613 235717 0.238941
14 0 binary 0/1 59986 Borough Enfield 0.491268 0.699366 0.189480 0.655668 0.300634 0.508732 0.810520 0.344332 1879 3536 0.031324 0.346999 1232 670 2866 647 1317 4098 5415 235717 0.243213
15 0 binary 0/1 59986 Borough Greenwich 0.357462 0.771409 0.207145 0.670917 0.228591 0.642538 0.792855 0.329083 2431 6942 0.040526 0.259362 1631 1438 5504 800 2238 7135 9373 235717 0.238771
16 0 binary 0/1 59986 Borough Hackney 0.355036 0.740840 0.223397 0.688393 0.259160 0.644964 0.776603 0.311607 2240 5676 0.037342 0.282971 1542 1268 4408 698 1966 5950 7916 235717 0.248358
17 0 binary 0/1 59986 Borough Hammersmith and Fulham 0.301404 0.765416 0.221176 0.714397 0.234584 0.698596 0.778824 0.285603 1278 3825 0.021305 0.250441 913 846 2979 365 1211 3892 5103 235717 0.237311
18 0 binary 0/1 59986 Borough Haringey 0.338848 0.789457 0.168154 0.720219 0.210543 0.661152 0.831846 0.279781 1830 5941 0.030507 0.235491 1318 999 4942 512 1511 6260 7771 235717 0.194441
19 0 binary 0/1 59986 Borough Harrow 0.496750 0.612069 0.214229 0.701950 0.387931 0.503250 0.785771 0.298050 1795 2530 0.029924 0.415029 1260 542 1988 535 1077 3248 4325 235717 0.249017
20 0 binary 0/1 59986 Borough Havering 0.423230 0.724281 0.242313 0.618638 0.275719 0.576770 0.757687 0.381362 1395 2992 0.023255 0.317985 863 725 2267 532 1257 3130 4387 235717 0.286528
21 0 binary 0/1 59986 Borough Hillingdon 0.455311 0.712025 0.210724 0.644411 0.287975 0.544689 0.789276 0.355589 1977 3991 0.032958 0.331267 1274 841 3150 703 1544 4424 5968 235717 0.258713
22 0 binary 0/1 59986 Borough Hounslow 0.368680 0.802988 0.195095 0.634146 0.197012 0.631320 0.804905 0.365854 1435 4608 0.023922 0.237465 910 899 3709 525 1424 4619 6043 235717 0.235645
23 0 binary 0/1 59986 Borough Islington 0.304659 0.788087 0.194818 0.717232 0.211913 0.695341 0.805182 0.282768 1503 4979 0.025056 0.231873 1078 970 4009 425 1395 5087 6482 235717 0.215211
24 0 binary 0/1 59986 Borough Kensington and Chelsea 0.317835 0.738176 0.243196 0.703176 0.261824 0.682165 0.756804 0.296824 1543 4042 0.025723 0.276276 1085 983 3059 458 1441 4144 5585 235717 0.258013
25 0 binary 0/1 59986 Borough Kingston upon Thames 0.326209 0.743986 0.224261 0.710867 0.256014 0.673791 0.775739 0.289133 1003 2671 0.016721 0.272999 713 599 2072 290 889 2785 3674 235717 0.241971
26 0 binary 0/1 59986 Borough Lambeth 0.331191 0.749942 0.214191 0.711842 0.250058 0.668809 0.785809 0.288158 3040 8259 0.050678 0.269050 2164 1769 6490 876 2645 8654 11299 235717 0.234092
27 0 binary 0/1 59986 Borough Lewisham 0.383681 0.737737 0.259566 0.619621 0.262263 0.616319 0.740434 0.380379 1743 4103 0.029057 0.298153 1080 1065 3038 663 1728 4118 5846 235717 0.295587
28 0 binary 0/1 59986 Borough Merton 0.398455 0.739511 0.211897 0.664186 0.260489 0.601545 0.788103 0.335814 1075 2572 0.017921 0.294763 714 545 2027 361 906 2741 3647 235717 0.248423
29 0 binary 0/1 59986 Borough Newham 0.329325 0.794005 0.188704 0.694333 0.205995 0.670675 0.811296 0.305667 3494 11526 0.058247 0.232623 2426 2175 9351 1068 3243 11777 15020 235717 0.215912
30 0 binary 0/1 59986 Borough Redbridge 0.431083 0.728098 0.193816 0.672131 0.271902 0.568917 0.806184 0.327869 2318 5175 0.038642 0.309355 1558 1003 4172 760 1763 5730 7493 235717 0.235286
31 0 binary 0/1 59986 Borough Richmond upon Thames 0.308970 0.793171 0.203722 0.695082 0.206829 0.691030 0.796278 0.304918 610 2042 0.010169 0.230015 424 416 1626 186 602 2050 2652 235717 0.226998
32 0 binary 0/1 59986 Borough Southwark 0.313562 0.810612 0.192847 0.681601 0.189388 0.686438 0.807153 0.318399 2723 9842 0.045394 0.216713 1856 1898 7944 867 2765 9800 12565 235717 0.220056
33 0 binary 0/1 59986 Borough Sutton 0.422572 0.720241 0.207940 0.669065 0.279759 0.577428 0.792060 0.330935 973 2116 0.016220 0.314989 651 440 1676 322 762 2327 3089 235717 0.246682
34 0 binary 0/1 59986 Borough Tower Hamlets 0.148666 0.912090 0.221854 0.659389 0.087910 0.851334 0.778146 0.340611 1374 12080 0.022905 0.102126 906 2680 9400 468 3148 10306 13454 235717 0.233982
35 0 binary 0/1 59986 Borough Waltham Forest 0.331851 0.813891 0.179779 0.677472 0.186109 0.668149 0.820221 0.322528 1851 6686 0.030857 0.216821 1254 1202 5484 597 1799 6738 8537 235717 0.210730
36 0 binary 0/1 59986 Borough Wandsworth 0.375945 0.693222 0.223756 0.718187 0.306778 0.624055 0.776244 0.281813 1941 4058 0.032358 0.323554 1394 908 3150 547 1455 4544 5999 235717 0.242540
37 0 binary 0/1 59986 Borough Westminster 0.235645 0.828441 0.218903 0.705611 0.171559 0.764355 0.781097 0.294389 2816 12284 0.046944 0.186490 1987 2689 9595 829 3518 11582 15100 235717 0.232980

Al revisar la tasa de falsos positivos, se localiza un sesgo muy pronunciado en Tower Hamlets. Para este distrito, el modelo seleccionado comete muchos menos errores de tipo I que para el resto de distritos. En el caso contrario se encuentra Harrow, con un FPR algo superior al resto, aunque no tan pronunciado como Tower Hamlets; Harrow es, además, uno de los distritos con menor criminalidad de Londres.

In [28]:
# Gráfica para FPR y FNR
ap.disparity(bdf, metrics, 'Borough')
Out[28]:

En cuanto a los falsos negativos, aunque sigue habiendo sesgo en Tower Hamlets y Harrow, es muy poco acusado y está muy cerca del umbral predefinido como aceptable.

En las siguientes gráficas pueden visualizarse las tasas para los distritos con más representación, donde distritos muy turísticos como Westminster o Camden tienen un FPR inferior. En el caso del FNR, sólo Tower Hamlets aparece entre los distritos con más representación que incumplen el umbral de equidad establecido.

In [29]:
# Se muestra el FPR de los grupos con más representación
f = Fairness()
fig, ax = plt.subplots(figsize=(10,8))
ax.set_title('Tasa de falsos positivos por distrito', fontsize=14)
fdf = f.get_group_value_fairness(bdf[bdf['attribute_name']=='Borough'].copy(deep=True))
fpr_fairness = aqp.plot_fairness_group(fdf, group_metric='fpr', min_group_size=0.035, ax=ax)
In [30]:
# Se muestra el FNR de los grupos con más representación
f = Fairness()
fig, ax = plt.subplots(figsize=(10,8))
ax.set_title('Tasa de falsos negativos por distrito', fontsize=14)
fdf = f.get_group_value_fairness(bdf[bdf['attribute_name']=='Borough'].copy(deep=True))
fnr_fairness = aqp.plot_fairness_group(fdf, group_metric='fnr', min_group_size=0.035, ax=ax)

Con la función summary se muestra el resumen de las métricas aplicadas para cada atributo y si se han cubierto los requisitos de equidad definidos (el umbral de tolerancia empleado es el establecido por defecto, 1.25).

In [31]:
# Resumen de la evaluación del modelo según las métricas examinadas y el umbral establecido
ap.summary(bdf, metrics)
Out[31]: